#   Copyright (C) 2008      Tri Tech Information Systems Inc.
# 
#   This program is free software; you can redistribute it and/or modify
#   it under the terms of the GNU General Public License as published by
#   the Free Software Foundation; either version 2 of the License, or
#   (at your option) any later version.
# 
#   This program is distributed in the hope that it will be useful,
#   but WITHOUT ANY WARRANTY; without even the implied warranty of
#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#   GNU General Public License for more details.
# 
#   You should have received a copy of the GNU General Public License along
#   with this program; if not, write to the Free Software Foundation, Inc.,
#   51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#   
import sys

from pyxx.wrapper import ExposedWrapper
from pyxx.formatters import *
from pyxx.parser import CodeModel
from pyxx.method import PickMethodWrapper
from pyxx.element import MemberVariableWrapper
from pyxx.callable import BaseFunctionWrapper
from pyxx.function import find_function
from pyxx.error import WrapperError

def check_variable(name, variable):
    """
    Returns True if this is a wrappable member variable
    """

    # do not wrap any array, at least for now
    if variable.type.arrayElements:
        return False

    return True

def filterMethods( meth, src, dst ):
    '''Move elements from src to dst based on meth
    '''
    if not callable(meth):
        # in this case, meth is a string
        name = meth
        def meth(v):
            return v.name == name

    to_remove = []
    for v in src:
        if meth(v):
            dst.append( v )
            to_remove.append(v)

    for v in to_remove:
        src.remove(v)




class ClassWrapper( ExposedWrapper ):

    WRAPPER_TEMPLATE = '''
        scope newclass = class_< %(templateargs)s >( %(args)s )
            %(methodlist)s
                ;
    '''                

    
    def __init__( self, ctx, klass, module,
            holder = None, noncopyable = None,
            no_init = None, python_name = None, is_interface = False ):

        ExposedWrapper.__init__( self, ctx, klass, python_name = python_name )
        self.module = module
        self.klass = klass
        
        self.abc = any([func.isAbstract for func in klass.functionMap.values()])

        if self.abc:
            if noncopyable is None:
                noncopyable = True
            if no_init is None:
                no_init = True

        # check for a private/protected copy/constructor
        for func in klass.functionMap.values():
            if func.name ==  klass.name:
                if len(func.arguments) == 1:
                    if func.arguments[0].type.isReference:
                        if list(func.arguments[0].type.qualifiedName) == list(klass.qualifiedName):
                            if func.accessPolicy != CodeModel.AccessPolicy.Public:
                                noncopyable = True
                                break


        self.holder = holder
        self.noncopyable = noncopyable
        self.no_init = no_init

        self.bases = list(klass.baseClasses)
        self.methods = [ PickMethodWrapper(v, self, is_interface)
                for k, v in klass.functionMap.items()
                if self.check_method(k, v) ]
        self.excludedMethods = []
        self.excludeMethod(lambda x: not self.check_method_access_policy(x))

        self.has_default_constructor = not [ 1 for k, v in klass.functionMap.items() if k == self.name]

        self.variables = [ MemberVariableWrapper(v, self)
                for k, v in klass.variableMap.items()
                if check_variable(k, v) and self.check_method_access_policy(v) ]

        self.excludedVariables = []

        self.wrapper_bases = []

        self.preparers = []

    def generate_wrapper_bases(self):
        return ": " + self.qualifiedName


    def setHoldType(self, holder):
        self.holder = holder

    def addPreparer(self, preparer):
        self.preparers.append( preparer )            


    def check_method_access_policy(self, method):
        return method.accessPolicy == CodeModel.AccessPolicy.Public

    def check_method(self, name, method):
        """
        Returns True if this is a wrappable method
        """
        if name[0] == '~':
            return False
        # we cannot wrap a template...
        if method.templateParameters:
            return False

        # assigment cannot be overloaded in python
        if method.name == "operator=":
            return False

        if method.isVariadics:
            return False

        # if this is a const version for which we also have a non-const version
        # skip it
        if method.isConstant:
            for meth_name, meth in self.functionMap.items():
                if meth_name == name and meth is not method:
                    if not meth.isConstant:
                        if list(meth.arguments) == list(method.arguments):
                            return False
        
        return True


    def do_prepare(self):
        for primary_base_class_name in self.bases:
            model = self.item.model
            ns = model.globalNamespace
        
            fullname = primary_base_class_name.split('::')

            thescope = list(self.item.scope)
            while True:
                primary_base_class = model.findItem( thescope + fullname, self.wrapper_ctx.fm)


                if not primary_base_class:
                    if thescope:
                        thescope.pop()
                        continue

                break                    

            
            if primary_base_class:
                name = tuple(primary_base_class.qualifiedName)
                try:
                    wrapper_base = self.wrapper_ctx.all_classes[ name ]
                    wrapper_base.prepare()
                    self.wrapper_bases.append( wrapper_base )
                except KeyError:
                    raise WrapperError("Could not locate base: %s for %s" % ("::".join(name), self.qualifiedName))
            
            else:
                print >> sys.stderr, "*** WARNING *** unable to find base class %s" % primary_base_class_name

        for wrapper_base in self.wrapper_bases:            
            self.preparers += wrapper_base.preparers

        for preparer in self.preparers:
            preparer(self)
        
        if self.abstract_methods():
            # so we have abstract methods
            # this means we cannot create objects of this class
            self.no_init = True
            self.noncopyable = True
            self.excludeMethod(self.name) # filter out all constructors


        for template_type in self.templateTypes():
            if template_type.endswith("const&"):
                template_type = template_type[:-6]
            self.addExtra("expose_template_type< %s >();" % template_type)

    def allTypes(self):
        types = set()

        for method in self.methods:
            types.update( method.allTypes() )

        return types            


    def templateTypes( self ):
        """
        Produce a list of template types used in the klass
        """

        types = set(map(str,self.allTypes()))
        types = filter(lambda s: s.find('<') != -1, types)
        return types

    def excludeMethod( self, meth ):
        '''Exclude a method by name, or by a function accepting the MethodWrapper as an argument
        '''
        filterMethods( meth, self.methods, self.excludedMethods )

    def excludeAllMethods( self ):
        '''Exclude all methods from the class wrapper
        '''
        self.excludedMethods += self.methods
        self.methods = []

    def includeMethod( self, meth ):
        '''Include a previously excluded method by name or function
        '''
        filterMethods( meth, self.excludedMethods, self.methods )

    def findMethod(self, name):
        for meth in self.methods:
            if meth.name == name:
                return meth
        else:
            raise KeyError(name)

    def findVariable(self, name):
        for var in self.variables:
            if var.name == name:
                return var
        else:
            raise KeyError(name)
            
    def abstract_methods(self):
        """
        Returns a set of all the methods which:
            are pure virtuals
            and have been filtered out
        """
        fns = set()

        # grab all the methods in my base class
        for base in self.wrapper_bases:
            fns.update( base.abstract_methods() )

        # for excluded methods its trickier
        for meth in self.excludedMethods + self.methods:
            if meth.isAbstract:
                # an excluded abstract method will prevent
                # a succesful subclassing
                fns.add( meth.name )
            else:
                # if its excluded, but present in impl
                # we are fine
                fns.discard( meth.name )

        return fns                

     

    def adoptMethod(self, method_name, cpp_name):
        """
        Take a free standing function and make it a method
        """
        self.excludeMethod(method_name)
        model = self.wrapper_ctx.cm
        funcs = find_function(self.wrapper_ctx.fm, model, cpp_name)

        for methodmodel in funcs:
            wrapper = BaseFunctionWrapper(methodmodel, None, 
                python_name = method_name, wrapper_ctx = self.wrapper_ctx)
            self.methods.append( wrapper )

        return wrapper
        
    def addGetter(self, name):
        self.addProperty( name, 'Get'+name )

    def addProperty( self, name, meth ):
        self.findMethod(meth).replaceDefault('''
        return extract< %(returnType)s >(self().attr("''' + name + '''"));
        ''')

    def excludeMemberVariable(self, name):
#        self.variables.remove(name)            
        filterMethods( name, self.variables, self.excludedVariables )

    def includeMemberVariable(self, name):
#        self.variables.remove(name)            
        filterMethods( name, self.excludedVariables, self.variables )

    def excludeAllMemberVariables(self):
        self.excludedVariables += self.variables
        self.variables = []            

    @formatter( indent )
    def generate_methodlist( self ):
        methods = self.methods
        if self.no_init:
            methods = [m for m in methods if m.name != self.name]
        methods = [ "." + m.generate() for m in self.methods ]

        statics = set()
        for meth in self.methods:
            if meth.isStatic:
                statics.add(meth.quotedName)

        for static in statics:
            methods.append(".staticmethod(%s)" % static)

        if self.has_default_constructor and not self.no_init:
            methods += self.add_default_constructor()
                   
        methods += [ "." + v.generate() for v in self.variables]                   
        return methods

    def addHeaderExtra(self, txt):
        self.header_extra += txt

    def add_default_constructor(self):
        return [".def(bpy::init<>())"]

    def __getitem__( self, item ):
        return self.findMethod(item)

    @generator
    @formatter( argument_list )
    def generate_args( self ):
        if self.docstring is not None:
            return [ self.quoted_python_name, self.docstring, 'no_init' ]
        else:
            return [ self.quoted_python_name, 'no_init' ]

    @formatter( argument_list )
    def generate_templateargs( self ):
        return [ '%(wrapped_class)s',
                self.holder,
                'bases<' + ",".join(self.bases) + ">",
                self.noncopyable and 'boost::noncopyable' or None ]

    def generate_wrapped_class(self):
        return self.qualifiedName

    def generate_readylines(self):
        def check_base(base):
            return "if( !is_wrapped<%s>() ) return false;" % base

        return map(check_base, self.bases)


    def addClassExtra(self, txt):
        self.classextra += txt

    def RegisterPtr(self, cppname):
        self.extras.append("register_ptr_to_python< %s >();" % cppname)           

    def excludeBaseClass(self, klass):
        self.bases.remove(klass)

    ExcludeBaseClass = excludeBaseClass        

    def ImplicitConversion(self, x, y):
        self.extras.append("bpy::implicitly_convertible< %s ,  %s >();" % (x, y) )


    def acceptPtrsByValue(self):
        self.extras.append("pass_ptr_by_value< %s >();" % self.name)

    def acceptPtrsByReference(self):
        self.extras.append("pass_ptr_by_reference< %s >();" % self.name)



